package main

import (
	"archive/zip"
	"context"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"
)

const (
	defaultChunkSize   = 5 * 1024 * 1024  // 默认分片大小5MB
	defaultConcurrency = 16               // 默认并发数
	defaultTimeout     = 30 * time.Minute // 默认超时时间
	defaultBufferSize  = 32 * 1024        // 默认缓冲区大小32KB
)

// DownloadConfig 下载配置
type DownloadConfig struct {
	URLs             []string              // 下载URL列表（主备）
	OutputPath       string                // 输出文件路径
	ChunkSize        int64                 // 分片大小(字节)
	MaxConcurrency   int                   // 最大并发数
	Timeout          time.Duration         // 超时时间
	ProgressInterval time.Duration         // 进度更新间隔
	ProgressFunc     func(percent float64) // 进度回调函数
}

// DownloadWithFallback 带备用URL的并发下载
func DownloadWithFallback(cfg DownloadConfig) error {
	if len(cfg.URLs) == 0 {
		return fmt.Errorf("至少需要一个下载URL")
	}

	// 设置默认值
	if cfg.ChunkSize <= 0 {
		cfg.ChunkSize = defaultChunkSize
	}
	if cfg.MaxConcurrency <= 0 {
		cfg.MaxConcurrency = defaultConcurrency
	}
	if cfg.Timeout <= 0 {
		cfg.Timeout = defaultTimeout
	}
	if cfg.ProgressInterval <= 0 {
		cfg.ProgressInterval = 500 * time.Millisecond
	}

	var lastErr error
	for _, url := range cfg.URLs {
		err := ConcurrentDownload(url, cfg)
		if err == nil {
			return nil
		}

		lastErr = err
		if is404Error(err) {
			continue
		}
		if isTimeoutError(err) {
			continue
		}
	}

	return fmt.Errorf("所有下载源尝试失败: %w", lastErr)
}

// ConcurrentDownload 并发分片下载实现
func ConcurrentDownload(url string, cfg DownloadConfig) error {
	// 获取文件大小
	size, err := getFileSize(url)
	if err != nil {
		return err
	}

	// 创建输出文件
	file, err := os.Create(cfg.OutputPath)
	if err != nil {
		return fmt.Errorf("创建文件失败: %w", err)
	}
	defer file.Close()

	// 预分配磁盘空间
	if err := file.Truncate(size); err != nil {
		return fmt.Errorf("预分配空间失败: %w", err)
	}

	ctx, cancel := context.WithTimeout(context.Background(), cfg.Timeout)
	defer cancel()

	var wg sync.WaitGroup
	// sem 变量声明后未使用，移除该声明
	errCh := make(chan error, 1) // 错误通道

	// 进度跟踪
	var progress int64
	var mu sync.Mutex
	stopProgress := make(chan struct{})

	// 启动进度显示
	go func() {
		ticker := time.NewTicker(cfg.ProgressInterval)
		defer ticker.Stop()

		for {
			select {
			case <-ticker.C:
				mu.Lock()
				p := progress
				mu.Unlock()
				percent := float64(p) * 100 / float64(size)
				if cfg.ProgressFunc != nil {
					cfg.ProgressFunc(percent)
				}
			case <-stopProgress:
				return
			case <-ctx.Done():
				return
			}
		}
	}()

	// 计算分片数量
	chunks := int(size / cfg.ChunkSize)
	if size%cfg.ChunkSize != 0 {
		chunks++
	}

	// 为每个分片创建单独的文件句柄，避免并发写入冲突
	type chunkTask struct {
		index int
		start int64
		end   int64
	}
	tasks := make(chan chunkTask, chunks)

	// 准备所有分片任务
	for i := 0; i < chunks; i++ {
		start := int64(i) * cfg.ChunkSize
		end := start + cfg.ChunkSize - 1
		if end >= size {
			end = size - 1
		}
		tasks <- chunkTask{i, start, end}
	}
	close(tasks)

	// 启动工作goroutine
	for i := 0; i < cfg.MaxConcurrency; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()

			for task := range tasks {
				select {
				case <-ctx.Done():
					return
				default:
				}

				// 每个分片使用独立的文件句柄
				f, err := os.OpenFile(cfg.OutputPath, os.O_WRONLY, 0o644)
				if err != nil {
					select {
					case errCh <- fmt.Errorf("打开文件失败: %w", err):
					default:
					}
					return
				}

				// 定位到分片起始位置
				if _, err := f.Seek(task.start, 0); err != nil {
					f.Close()
					select {
					case errCh <- fmt.Errorf("文件定位失败: %w", err):
					default:
					}
					return
				}

				// 创建范围请求
				req, err := http.NewRequest("GET", url, nil)
				if err != nil {
					f.Close()
					select {
					case errCh <- fmt.Errorf("创建请求失败: %w", err):
					default:
					}
					return
				}
				req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", task.start, task.end))

				// 执行请求
				resp, err := http.DefaultClient.Do(req.WithContext(ctx))
				if err != nil {
					f.Close()
					select {
					case errCh <- fmt.Errorf("请求失败: %w", err):
					default:
					}
					return
				}

				if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent {
					resp.Body.Close()
					f.Close()
					select {
					case errCh <- fmt.Errorf("服务器返回错误状态码: %s", resp.Status):
					default:
					}
					return
				}

				// 复制数据
				buf := make([]byte, defaultBufferSize)
				n, err := io.CopyBuffer(f, resp.Body, buf)

				// 关闭资源
				resp.Body.Close()
				if err := f.Close(); err != nil {
					select {
					case errCh <- fmt.Errorf("关闭文件失败: %w", err):
					default:
					}
					return
				}

				if err != nil {
					select {
					case errCh <- fmt.Errorf("复制数据失败: %w", err):
					default:
					}
					return
				}

				// 更新进度
				mu.Lock()
				progress += n
				mu.Unlock()
			}
		}()
	}

	// 等待所有分片完成
	done := make(chan struct{})
	go func() {
		wg.Wait()
		close(done)
	}()

	select {
	case <-done:
		close(stopProgress)
		return nil
	case err := <-errCh:
		cancel()
		close(stopProgress)
		return err
	case <-ctx.Done():
		close(stopProgress)
		return ctx.Err()
	}
}

// getFileSize 获取远程文件大小
func getFileSize(url string) (int64, error) {
	resp, err := http.Head(url)
	if err != nil {
		return 0, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return 0, fmt.Errorf("服务器返回非200状态码: %s", resp.Status)
	}

	return resp.ContentLength, nil
}

func is404Error(err error) bool {
	if err == nil {
		return false
	}
	return strings.Contains(err.Error(), "404 Not Found")
}

func isTimeoutError(err error) bool {
	if err == nil {
		return false
	}
	timeoutErrors := []string{
		"timeout",
		"timed out",
		"context deadline exceeded",
		"use of closed network connection",
	}
	errStr := err.Error()
	for _, te := range timeoutErrors {
		if strings.Contains(errStr, te) {
			return true
		}
	}
	return false
}

// unzip 解压ZIP文件到指定目录
func unzip(src, dest string) error {
	r, err := zip.OpenReader(src)
	if err != nil {
		return err
	}
	defer r.Close()

	// 创建目标目录
	err = os.MkdirAll(dest, 0o755)
	if err != nil {
		return err
	}

	// 遍历ZIP文件中的每个文件/目录
	for _, f := range r.File {
		// 防止路径遍历攻击
		fpath := filepath.Join(dest, f.Name)
		if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) {
			return fmt.Errorf("非法文件路径: %s", fpath)
		}

		if f.FileInfo().IsDir() {
			// 创建目录
			err = os.MkdirAll(fpath, 0o755)
			if err != nil {
				return err
			}
			continue
		}

		// 确保文件所在目录存在
		if err = os.MkdirAll(filepath.Dir(fpath), 0o755); err != nil {
			return err
		}

		// 创建目标文件
		outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
		if err != nil {
			return err
		}

		// 打开ZIP中的文件
		rc, err := f.Open()
		if err != nil {
			outFile.Close()
			return err
		}

		// 复制文件内容
		_, err = io.Copy(outFile, rc)

		// 关闭文件描述符
		outFile.Close()
		rc.Close()

		if err != nil {
			return err
		}
	}

	return nil
}
